/* 
 * File:   Scene.cpp
 * Author: brady
 * 
 * Created on February 2, 2011, 7:04 PM
 */

#include <vector>

#include "Scene.h"
#include "Shader.h"

Scene::Scene() {
    BGColor = Vector3D(0.0, 0.0, 0.0);
    Tmin = ComputeEpsilon();
    Tmax = 100.00;
    RDepth = 10;
    useBVH = true;
    SamplesPerPixel = 1;
    SoftShading = true;
    differenceThreshold = 0.01;
    preSampling = true;
}

double Scene::ComputeEpsilon()
{
    return (double) FLT_EPSILON * 2.0;
    
}

Scene::~Scene() {
}

void Scene::BuildBVH()
{
    BVHRoot = new BoundingVolumeNode(shapes, Shape::X_VAL);
}

bool Scene::CaptureImageMT(string path, Camera* camera, int resX, int resY, int threadCount)
{
    pthread_t threads[threadCount];
    camera->SetResolution(resX, resY);
    int size = resX * resY * 3;
    float* imageData = new float[size];
    if(useBVH){
        cout << "Building BVH... \n";
        cout.flush();
        BuildBVH();
        cout << "Done\n";
    }
    cout << "Building pixel stack...\n";
    for(int row = 0; row < resY; row++)
    {
        for(int col = 0; col<resX; col++)
        {
            Pixel* px = new Pixel;
            px->x = col;
            px->y = row;
            Pixels.push(px);
        }
    }
    cout << "Done\n";

    ThreadInfo ti;
    ti.camera = camera;
    ti.scene = this;
    ti.pixels = &Pixels;
    ti.imageData = imageData;
    ti.threadCount = threadCount;
    int origSize = Pixels.size();
    //spawn all my threads
    pthread_mutex_init(&pixelsMutex, NULL);
    pthread_mutex_init(&coutMutex, NULL);
    cout << "Rendering...\r";
    if (threadCount == 1)
    {
        RenderPixels((void*)&ti);
    }
    else
    {

        for (int i = 0; i < threadCount; i++)
        {
            pthread_create(&threads[i], NULL, RenderPixels, (void*) &ti);
        }


        //Progress counter
        int oldCompletion = 0;
        while (Pixels.size() > 0)
        {
            sleep(2);
            int newCompletion = (int) ((double) (origSize - Pixels.size()) / (double) origSize * 100);
            if (oldCompletion != newCompletion)
            {
                oldCompletion = newCompletion;
                cout << "                 \r";
                cout << "Rendering... "<< newCompletion << "%\r";
                cout.flush();
            }
        }

        //join them here
        for (int i = 0; i < threadCount; i++)
        {
            pthread_join(threads[i], NULL);
        }
    }
    cout <<"\nDone\n";
    PNGImage png;
    return png.writeFileData(path, resX, resY, imageData);
}

static void* RenderPixels(void* threadData) {
    ThreadInfo* ti = (ThreadInfo*)threadData;

    int TotalThreadSamples = 0;
    int TotalThreadPixels = 0;

    stack<Pixel*>* pixels = ti->pixels;
     
    Scene* scene = ti->scene;
    Camera* camera = ti->camera;
    float* imageData = ti->imageData;
    int threadCount = ti->threadCount;
    int oldCompletion = 0;
    int origSize = pixels->size();
    while(true){
        Pixel* pixel = NULL;

        //LOCK HERE

        if(threadCount == 1){
            if (pixels->size() >= 0) {
                    int newCompletion = (int) ((double) (origSize - pixels->size()) / (double) origSize * 100);
                    if (oldCompletion != newCompletion) {
                        oldCompletion = newCompletion;
                        cout << "                 \r";
                        cout << "Rendering... "<< newCompletion << "%\r";
                        cout.flush();
                    }
                }
        }

        pthread_mutex_lock(&pixelsMutex);
        if(!pixels->empty()){
            pixel = pixels->top();
            pixels->pop();
        }
         pthread_mutex_unlock(&pixelsMutex);
        //UNLOCK HERE
        if(pixel == NULL)
        {
            pthread_mutex_lock(&coutMutex);
            cout << "Thread " << pthread_self() << ": " << TotalThreadSamples << " samples over "<<TotalThreadPixels<<" pixels\n";
            pthread_mutex_unlock(&coutMutex);
            if(threadCount > 1)
            pthread_exit(NULL);
            return NULL;
        }

        Shader* frontShader = NULL;
        int row = pixel->y;
        int col = pixel->x;


        //do this for each spp
        Vector3D pxlColor(0.0, 0.0, 0.0);
        Vector3D finalColor(0.0, 0.0, 0.0);
        
        double psMinR, psMinG, psMinB, psMaxR, psMaxG, psMaxB;
        psMinR = 1.0;
        psMinG = 1.0;
        psMinB = 1.0;
        psMaxR = 0.0;
        psMaxG = 0.0;
        psMaxB = 0.0;

        int rMap[scene->SamplesPerPixel];
        int psRMap[4];

        RandomizeArray(rMap, scene->SamplesPerPixel);
        RandomizeArray(psRMap, 4);

        for(int i = 0; i<4; i++)
        {
            psRMap[i]= psRMap[i] - 4;
        }

//BEGIN SINGLE SAMPLE


        int s = (scene->preSampling? -4 : 0);
        int samplesTaken = 0;
        for (s; s < scene->SamplesPerPixel; s++) {

            Ray ray;
            ray = camera->ComputeRay(row, col, scene->SamplesPerPixel, s);
            ray.SetRecursionDepth(scene->RDepth);
            ray.RefractiveIndex.push_back(1.0);
            //remap sample
             if(s >= 0)
            {
                ray.sNum = rMap[s];
            }
             else 
             {
                 ray.sNum = psRMap[s + 4];
             }
            HitStruct hs;
            bool hit;
            if (scene->useBVH) {
                hit = scene->BVHRoot->TestIntersect(ray, scene->Tmin, scene->Tmax, hs);
                frontShader = hs.shader;
            } else {
                double closestT = DBL_MAX;
                for (int s = 0; s < scene->shapes.size(); s++) {
                    hit = scene->shapes[s]->TestIntersect(ray, scene->Tmin, scene->Tmax, hs);
                    if (hit && (hs.tVal < closestT)) {
                        closestT = hs.tVal;
                        frontShader = hs.shader;
                    }
                }
            }
            if (frontShader != NULL) {
                pxlColor = frontShader->Shade(ray, hs, scene);
            }
            else {
                pxlColor = scene->BGColor;
            }

            samplesTaken++;
            finalColor += pxlColor; 
            
            if(s < 0)
            {
                psMinR = min(pxlColor[0], psMinR);
                psMinG = min(pxlColor[1], psMinG);
                psMinB = min(pxlColor[2], psMinB);
                psMaxR = max(pxlColor[0], psMaxR);
                psMaxG = max(pxlColor[1], psMaxG);
                psMaxB = max(pxlColor[2], psMaxB);
            }
            if(s == -1)
            {
                if((psMaxR - psMinR < scene->differenceThreshold)&&
                   (psMaxG - psMinG < scene->differenceThreshold)&&
                   (psMaxB - psMinB < scene->differenceThreshold))
                    break; //we're within our threshold, use this data
                
                //if there's too much variation,
                //dump this color data and fully sample the pixel
                else finalColor = Vector3D(0.0, 0.0, 0.0);
                TotalThreadSamples += 4; //we've taken 4 pre-samples
                samplesTaken = 0;
            }
        }
            finalColor /= samplesTaken;
           // if(samplesTaken < scene->SamplesPerPixel)finalColor = Vector3D(0.0, 1.0, 1.0);
            int resX = camera->GetXResolution();
            int index = (row * resX * 3) + col * 3;
            imageData[index + 0] = (float) finalColor[0];
            imageData[index + 1] = (float) finalColor[1];
            imageData[index + 2] = (float) finalColor[2];

            TotalThreadSamples += samplesTaken;
            TotalThreadPixels++;
    //END RENDERING SINGLE PIXEL
    }
}

void Scene::AddCamera(Camera* camera)
{
    cameras.push_back(camera);
}

void Scene::AddShape(Shape* s)
{
    shapes.push_back(s);
}

void Scene::AddLight(Light* light)
{
    lights.push_back(light);
}

static void RandomizeArray(int* randMap, int samples)
{
    for(int i = 0; i<samples; i++)
    {
        randMap[i] = i;
    }

    for(int i = 0; i<samples; i++)
    {
        int temp;
        int swapIndex = random()%samples;
        temp = randMap[i];
        randMap[i] = randMap[swapIndex];
        randMap[swapIndex] = temp;
    }
}